/*
 *  Copyright (C) 2011 glauco
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.balisunrise.swing.components.focusManagers;

import java.awt.Component;
import java.awt.Container;
import java.awt.FocusTraversalPolicy;
import java.awt.KeyboardFocusManager;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

/**
 * Monitora a troca de focus entre os componentes de um painel.
 *
 * @author glauco
 */
public class JPanelFocusManager implements PropertyChangeListener,
        ContainerListener, AncestorListener{

    /**
     * Painel monitorado.
     */
    private JPanel panel;

    /**
     * Container o qual este JPanelFocusManager controla eventos de focus.
     */
    private Container container;

    /**
     * Respectivo do container.
     */
    private FocusTraversalPolicy ftp;

    /**
     * Último component focusable do container.
     */
    private Component lastComponent;

    /**
     * Component com o focus anterior.
     */
    private Component oldComponent;

    /**
     * Novo component com o focus.
     */
    private Component newComponent;

    /**
     * Listeners de eventos.
     */
    private List<FocusManagerListener> listeners;

    @SuppressWarnings("LeakingThisInConstructor")
    public JPanelFocusManager(JPanel panel) {
        this.panel = panel;
        listeners = new ArrayList<FocusManagerListener>();

        panel.addAncestorListener(this);

        activate();

        refreshComponents();
    }

    /**
     * Ativa o monitor do Focus.
     * Ao criar o JPanelFocusManager já é ativado, sendo apenas necessário
     * desativar.
     */
    public final void activate(){
        KeyboardFocusManager focusManager =
                KeyboardFocusManager.getCurrentKeyboardFocusManager();
        focusManager.addPropertyChangeListener(this);
    }

    /**
     * Desativa o monitor de Focus.
     */
    public final void deactivate(){
        KeyboardFocusManager focusManager =
                KeyboardFocusManager.getCurrentKeyboardFocusManager();
        focusManager.removePropertyChangeListener(this);
    }

    /**
     * Implementação de PropertyChangeListener.
     */
    public void propertyChange(PropertyChangeEvent evt) {

        if (container == null)
            return ;
        
        if(evt.getPropertyName().equals("focusOwner")){

            Component o = null;
            Component n = null;

            if(evt.getOldValue() != null &&
                    evt.getOldValue() instanceof Component)
                o = (Component) evt.getOldValue();
            if(evt.getNewValue() != null &&
                    evt.getNewValue() instanceof Component)
                n = (Component) evt.getNewValue();

            processFocusOwner(o,n);
        }
    }

    private void processFocusOwner(Component oldValue, Component newValue){
        if (oldComponent == null)
            oldComponent = oldValue;
        if (newValue != null && !newValue.equals(oldComponent))
            newComponent = newValue;

        if(newComponent != null & oldComponent != null){

            // Verifica se o focus dos componentes é o mesmo do ciclo monitorado
            if (newComponent.isFocusCycleRoot(container)
                    && oldComponent.isFocusCycleRoot(container)){

                // pega o container pai de cada componente
                Container newParent = newComponent.getParent();
                Container oldParent = oldComponent.getParent();

                // se o container pai for o painel, ou o container pai dessende
                // do painel, então processa o evento.
                if( (newParent == panel || panel.isAncestorOf(newParent))
                        &&(oldParent == panel || panel.isAncestorOf(oldParent)))
                    fireFocusTravelEvent();
            }

            oldComponent = null;
            newComponent = null;
        }
    }

    private void fireFocusTravelEvent(){

        List<Component> travesedComponents = new ArrayList<Component>();
        Component actualComponent = oldComponent;
        boolean cycleDown = false;

        // percorre o componente atual, que era onde o focus estava,
        // até o componente onde o focus está,
        // No caso do focus atual estar em um componente anterior ao componente
        // onde o focus estava, percorre até o último camponente da janela,
        while(!actualComponent.equals(newComponent) &&
                !actualComponent.equals(lastComponent)){

            travesedComponents.add(actualComponent);

            try{
                actualComponent = ftp.getComponentAfter(container,
                        actualComponent);

                // se chegar no novo componente é porque o focus foi para baixo
                if(actualComponent.equals(newComponent))
                    cycleDown = true;

            }catch(Exception ex){
                break;
            }
        }

        // cria evento
        FocusTraversalEvent evt = new FocusTraversalEvent(
                oldComponent, newComponent, container,
                travesedComponents, cycleDown);

        // dispara os listeners
        for (FocusManagerListener listener : listeners) {
            listener.focusTravesed(evt);
        }

        //System.out.println(evt.toString());
    }

    /**
     * Implementação de ContainerListener.
     * Dispara refreshComponents.
     * @param e
     */
    public void componentAdded(ContainerEvent e) {
        refreshComponents();
    }

    /**
     * Implementação de ContainerListener.
     * Dispara refreshComponents.
     * @param e
     */
    public void componentRemoved(ContainerEvent e) {
        refreshComponents();
    }

    /**
     * Atualiza os componentes do JPanel, os quais são: container, ftp
     * e lastComponent.
     * Adiciona <code>this</code> como ContainerListener ao container.
     */
    private void refreshComponents(){

        if(container != null)
            container.removeContainerListener(this);

        container = panel.getFocusCycleRootAncestor();

        if(container == null)
            return ;
        
        container.addContainerListener(this);
        ftp = container.getFocusTraversalPolicy();
        lastComponent = ftp.getLastComponent(container);
    }

    /**
     * Adiciona um Listener do Focus Manager.
     */
    public void addFocusManagerListener(FocusManagerListener listener){
        listeners.add(listener);
    }

    /**
     * Remove um Listener do Focus Manager.
     */
    public void removeFocusManagerListener(FocusManagerListener listener){
        listeners.remove(listener);
    }

    /**
     * Lista em um array os listeners do Focus Manager.
     */
    public FocusManagerListener[] getFocusManagerListener(){
        return listeners.toArray(new FocusManagerListener[]{});
    }

    /**
     * Implementação de AncestorListener
     */
    public void ancestorAdded(AncestorEvent event) {
        refreshComponents();
        //System.out.println("add");
    }

    /**
     * Implementação de AncestorListener
     */
    public void ancestorRemoved(AncestorEvent event) {
        refreshComponents();
        //System.out.println("rem");
    }

    /**
     * Implementação de AncestorListener
     */
    public void ancestorMoved(AncestorEvent event) {
        // nada
    }

}
